menu
  Home  ==>  papers  ==>  oop_components  ==>  delphi_livebindings_spelunking   

Delphi LiveBindings Spelunking - Felix John COLIBRI.

  • abstract : analysis of the architecture of the Delphi LiveBindings : how the tBindingExpression compiles a String expression to build an environment referencing objects which can be evaluated to fill component properties. Dump of the pseudo code and UML Class Diagram of the LiveBinding architecture
  • key words : tBindingExpression, iScope, iValue, tNestedScope, tBindingOutput, tCompiledBinding - Interface to Object conversion
  • software used : Windows XP Home, Delphi XE2, update 1 installed
  • hardware used : Pentium 2.800Mhz, 512 M memory, 140 G hard disc
  • scope : Delphi XE2, Vcl and FireMonkey
  • level : Delphi developer
  • plan :


1 - Delphi LiveBindings

Livebindings are the new data binding technique which allows us to link objects to each other using String expressions.

Delphi LiveBindings is available for both the Vcl and FireMonkey. For the Vcl, it allows us to bind tWinControls for which no db_xxx data sensitive controls were provided. And for Firemonkey it is at the heart of the database visual management, since no db_xxx are available : linking any FireMonkey visual control to a Database using LiveBinding is possible.



In this article, we will explore the architecture of LiveBindings using the simplest possible binding.

But before starting the deep dive, let us stress that going this deep into the innards of LiveBindings is not at all required in order to implement LiveBindings in your applications. Daily use of LiveBindings is quite simple and straightforward.

However, knowing what's under the hood, especially given the current state of the documentation, could not hurt too much.



To make sure that readers casually looking at the article understand that LiveBinding is easy, we will first present 3 tutorial examples :

  • binding a tColorPanel to to the color of a tRectangle
  • displaying the CATEGORY field of the BIOLIFE (FISHFACTS) Database Table to a tEdit
  • displaying the tPerson.FirstName in a tLabel.Caption using a simple tMemo.Lines expression



2 - LiveBindings Tutorial

2.1 - A tColorPanel changes the target tRectangle color

For this first example we want to use a tColorPanel (a kind of 2D tTrackBar used to define color values) in order to change a tRectangle color.

In this case

  • the source (the origin) is the tColorPanel
  • the target (what is changed by the LiveBinding) is the tRectangle color
In traditional Vcl Database binding, we use
  • a tDataSource as the source
  • a db_xxx control as the target (the tWinControl)
and this explains the "source -> control" terminology.

In order to link the tColorPanel changes to the color of the tRectangle, we have to use a tBindExpression which has 4 parameters

  • for the source
    • SourceComponent, here ColorPanel1
    • SourceExpression, here ColorPanel1.Color
  • for the target (the "control")
    • ControlComponent, which is Rectangle1
    • ControlExpression, in our case Rectangle1.Fill.Color


To build this Firemonkey example
   create a FireMonkey project with "File | New | FireMonkey HD Application"
   drop a tColorPanel on the Form, name it source_color_panel
   drop a tRectangle on the Form, name it target_rectangle

   make sure target_rectangle_ is selected
   in the Object Inspector, select the LiveBinding property, Click on it and select "New LiveBinding ..."
   a "New LiveBinding is displayed
   select tBindExpression and click on it
   a BindingsList1 is automatically added to Form1, and a new tBindEpression is added as a child component of target_rectangle_, and automatically named BindExpressiontarget_rectangle_1

   in the top left corner, in the Structure TreeView1 click target_rectangle_, LiveBindings, BindExpressiontarget_rectangle_1
   BindExpressiontarget_rectangle_1 is displayed in the Object Inspector :

tcolorpanel_livebinding

where you see

  • in red the BindExpressiontarget_rectangle_1 in the Structure TreeView
  • in green the SourceComponent and SourceExpression properties
  • in grey the ControlComponent (target_rectangle_) and ControlExpression
   set the 4 LiveBinding parameters
  • in SourceComponent, open the combobox and select source_color_panel_
  • in SourceExpression type Color
  • in ControlComponent, keep target_rectangle_ selected
  • in ControlExpression, type Fill.Color

   compile and run

   the target_rectangle_ automatically takes the default source_color_panel green color:

livebinding_initial_evaluation



However any change in the source_color_panel is not automatically forwarded to the target_rectangle_. To improve this we must trigger the change from source_color_panel_.OnChange event, and call BindingsList1.Notify.

Here is the code:
   select source_color_panel_ and create its OnChange event
   in this event, type the code which tells BindingsList1 to update the color when the source_color_panel_.Color changes :

Procedure TForm1.source_color_panel_Change(SenderTObject);
  Begin
    BindingsList1.Notify(source_color_panel_'Color');
  End;

   compile, run, and change the source tColorPanel trackbar positions
   the target rectangle color changes

bindinglist_notify



Two possible initial mistakes

  • the confusion about the "source" and the "control". Both are "tWinControls". Simply keep the database analogy in mind (the original information comes from the tDataSource and flows towards the tDbEdit control)
  • to which component should the tBindExpression added. Database to the rescue again: we link from the tDbEdit to the tDataSource. Therefore, from the target_rectangle_ back to the source_color_panel_. The reason in both case is the 1-n multiplicity: it is easier to specify on the target side which is the only possible source (rather than having a list to specify all the possible targets)


2.2 - Linking a tEdit to a tDataSet

In FireMonkey, there are no "data sensitive" controls, like the tDbEdit Vcl control. In FireMonkey, LiveBindings are used to build database applications. And linking any control to the database is really simple.

Here is how to link the CATEGORY field of the FISHFACT table to a FireMonkey tEdit.

First we setup a FireMonkey application with an open tDataSet :
   create a FireMonkey project with "File | New | FireMonkey HD Application" and save it in a folder
   from

  C:\Program Files\Common Files\CodeGear Shared\Data\

copy the FISHFACTS.CDS and paste it in the same folder as the .DPR. Actually, you can use any .CDS, or link the any database using dbExpress, or ADO. We simply chose the local file to avoid detailing the connection

   drop a tClientDataSource on Form1 and
  • in FileName, type FISHFACTS.CDS
  • toggle Active to True
Alternately just set up any tDataSet descendent, and open it
   drop a tDataSource component, and link DataSource1.DataSet to ClientDataSet1


Now the LiveBindings part
   drop a tEdit on Form1
   in the Object Inspector, select its LiveBindings property, open the combobox and select "Link to DbField ..."
   the New DbLink is opened, with all the fields of the tDataSet
   select any String field, say CATEGORY
   a BindingsList1 and a BindScopeDb have been automatically added to Form1 and the value of the first CATEGORY ("TriggerFish") is displayed

firemonkey_livebinding



That's it. To move to the next row in the DataBase, we can add a BindNavigator, and use LiveBinding to allow navigation:
   drop a BindNavigator on Form1
   in the Object Inspector, select BindScope and select BindScope1
   compile, run and navigate !


2.3 - LiveBinding by code

To understand the LiveBinding architecture, we will use the simplest LiveBinding possible, which is the evaluation of a StringExpression.

We will use

  • a tPerson Class with a FirstName property defined by

    Type tperson =
           Class(TObject)
               Private
                 FFirstNameString;
               Published
                 Property Firstname : String read FFirstName write FFirstName;
            End;

  • the following source expression:

      "nickname " + my_friend.FirstName

  • the target is a tLabel.Caption


Here is the code:
   create a VCL application (Firemonkey would work in exactly the same manner)
   create the c_person Class

   drop a tMemo, name it source_expression_memo_, and in Lines type the source expression
   drop a tButton to perform the computation and name it evaluate_expression_:
   drop a target tLabel control and name it target_label_
   add System.Bindings.ExpressionDefaults and System.Bindings.Expression to the Uses clause
   in the evaluate_expression OnClick, type the following code:

Var g_c_persontPersonNil;
    g_c_binding_expressiontBindingExpressionDefaultNil;

Procedure TForm1.evaluate_expression_Click(SenderTObject);
  Begin
    g_c_person:= tPerson.Create;
    g_c_person.Firstname:= 'Archi';

    g_c_binding_expression:= tBindingExpressionDefault.Create;

    With g_c_binding_expression Do
    Begin
      Source:= source_expression_memo_.Lines.Text;
      Compile([
         tBindingAssociation.Create(g_c_person'my_friend')
         ]);
      Outputs.Add(target_label_'Caption');
      EvaluateOutputs;
    End;
  End// evaluate_expression_Click

Where

  • the g_c_binding_expression is the main component to compile the expression
  • The Source property will contain the String to be evaluated
  • the Compile call receives a tBindingAssociation object which is used to link
    • the g_c_person object
    • an the "my_friend" string used in the source expression
  • Outputs.Add specifies the target and the receiving property
  • EvaluateOutputs uses the compiled expression to place the result int the label's caption
   compile and run
   here is the result

livebinding_expression_evaluation




3 - LiveBindings Internals

3.1 - LiveBinding Architecture

Our goal is to be able to draw the UML Class Diagram of a simple LiveBinding example.



We used the simple expression evaluator presented above, and simple decomposed the code in several parts to be able to display the different elements involved in LiveBinding. And this allowed us to draw the UML Class Diagram.



In the code used for analysis, we used the following parts :

  • two Classes

    Type c_person =
           Class(TObject)
               Private
                 FFirstNameString;
                 FLastNameString;
                 FAgeInteger;
               Published
                 Function ToStringStringOverride;

                 Property Firstname : String read FFirstName write FFirstName;
                 Property Lastname : String read FLastName write FLastName;
                 Property Age : Integer read FAge write FAge;
            End;

          c_company=
            Class(tObject)
               Private
                 fCompanyString;
               Published
                 Property Company : String read FCompany write FCompany;
            End;

    Function c_person.ToStringString;
      Begin
        Result:= FirstName' 'IntToStr(Age);
      End// ToString

    Procedure create_objects;
      Begin
        g_c_person:= c_person.Create;
        g_c_person.Firstname:= 'Archibald';
        g_c_person.LastName:= 'LERICH';
        g_c_person.Age:= 33;

        g_c_company:= c_company.Create;
        g_c_company.Company:= 'CRESUS Inc';
      End// create_objects

  • two method to compile and to evaluate the expression:

    Procedure create_and_compile(p_sourceString);
      Var l_c_person_binding_associationtBindingAssociation;
          l_c_company_binding_associationtBindingAssociation;
          l_c_amount_binding_associationtBindingAssociation;
      Begin
        l_c_person_binding_association:= tBindingAssociation.Create(g_c_person'the_person');
        l_c_company_binding_association:= tBindingAssociation.Create(g_c_company'the_company');

        g_c_binding_expression:= c_my_binding_expression_default.Create;

        With g_c_binding_expression Do
        Begin
          Source:= p_source;

          Compile([
                l_c_person_binding_association,
                l_c_company_binding_association
              ]);
        End// with g_c_binding_expression
      End// create_and_compile

    Procedure set_outputs_and_evaluate(p_c_output_labeltLabel);
      Begin
        With g_c_binding_expression Do
        Begin
          Outputs.Add(p_c_output_label'Caption');
          EvaluateOutputs;
        End// with g_c_binding_expression
      End// set_outputs_and_evaluate

  • the main program contains a tMemo for the source String. Our example string is:

        source "boss "+ the_person.FirstName
        + " "+ the_person.LastName
        + " cy "+ the_company.Company

    and several buttons to call the creation of the objects, the compilation, the evaluation, and several display methods which will be presented now.



3.2 - Creating an identifier / value environment

3.2.1 - What's in RootScope ?

Our main purpose is to be able to explain and display the different objects involved into a simple expressions LiveBindings.

We quickly run into an Interface problem. Many properties were defined as Interfaces but to display anything, we have to find out what the implementing object is in order to investigate its content.

For instance, a tBindingExpressionDefault has a RootScope, which is defined as an iScope :

IScope = 
  Interface
    Function Lookup(Const NameString): IInterface;
  End;

TBindingExpressionDefault = 
  Class(TBindingExpression, ...
    Protected
      Property RootScopeIScope read FRootScope implements IScope;
      ...
  End;

So, basically, a scope is an environment which can be looked up. But what's in this environment and how is it organized ?

Unless we know a little bit more about the object implementing iScope in this case, we cannot display too much.

So we had to invest into Rtti, until we managed to display the RootScope.



3.3 - tDictionaryScope

To build the environment, the compiler needs to initialize ("register") basic litterals and operators.

This is performed by MakeBasicConstants() and MakeBasicOperators() in EVALSYS.

Basically MakeBasicConstants creates a tValueWrapper around the value True (or the value False, Nil or Pi).

A tValueWrapper wraps litteral values, and can be used to fetch the type and the value. It is defined by

TValueWrapper = 
  Class(TInterfacedObjectIWrapperIValue)
    Private
      FValueTValue;
    Public
      Constructor Create(Const AValueTValue);

      { IValue }
      Function GetValueTValue;
      Function GetTypePTypeInfo;
  End;

with the following Interfaces :

IWrapper = 
  Interface
  End;

IValue = 
  Interface
    //Used to obtain the type information for the actual value
    Function GetTypePTypeInfo;
    Function GetValueTValue;
  End;



Here is how to create a tValueWrapper and display the value

Var l_c_TRUE_value_wrappertValueWrapper;

  l_c_TRUE_value_wrapper:= TValueWrapper.Create(true);
  display(l_c_TRUE_value_wrapper.GetValue.ToString);



Those litteral values are grouped into dictionaries, namely a tDictionaryScope which are used to lookup the litteral name, say 'True' to get the value wrapper which returns True.

The tDictionaryScope is defined by

TDictionaryScope = 
  Class(TInterfacedObjectIScopeIScopeExIScopeEnumerable)
    Public
      Type
        TMap = TDictionary<StringIInterface>;
      Constructor Create;
      Property MapTMap read FMap;

      { IScope }
      Function Lookup(Const NameString): IInterfaceoverload;
    ...
  End;



We can add our "True" wrapper with this code:

Var l_c_dictionary_scopeTDictionaryScope;

  l_c_dictionary_scope := TDictionaryScope.Create;
  l_c_dictionary_scope.Map.Add('True'l_c_TRUE_value_wrapper);

and we can lookup the 'True' string using:

Var l_c_lookup_value_wrappertValueWrapper;

  l_c_lookup_value_wrapper:= l_c_dictionary_scope.Lookup('True'As tValueWrapper;
  display(l_c_lookup_value_wrapper.GetValue.ToString);



After those first trials, we will now start our expression LiveBinding



3.4 - Step 1 : Creating the tBindingAssociation

3.4.1 - tBindingAssociation

We first stores the relation between a Delphi object and a expression object using a tBindingAssociation Record :

TBindingAssociation = 
  Record
    Public
      RealObjectTObject;
      ScriptObjectString;

      Constructor Create(ARealObjectTObjectConst AScriptObjectString);
  End;



Here is our code:

Var l_c_person_binding_associationtBindingAssociation;

  l_c_person_binding_association:= 
      tBindingAssociation.Create(g_c_person'the_person');

and we can obviously redisplay this association Record

With l_c_person_binding_association Do
Begin
  display('real_object 'RealObject.ClassName);
  display('real_object.ToString 'RealObject.ToString);
  display('ScriptObject: 'ScriptObject'<');
End;



3.5 - Step 2 : Creating and compiling the tBindingExpression

3.5.1 - tBindingExpression

The workhorse of the LiveBindings is the tBindingExpression. This Class contains
  • a dictionary of the binding associations
  • a scope for the outputs
  • Compile and evaluateOutputs methods
The Class is defined as:

TBindingExpression =
  Class Abstract(TObject)
    Public
      Type
        TAssociationPair = TPair<TObjectString>;
        TAssociations = TDictionary<TObjectString>;

      Procedure Compile(Const AssocsArray Of TBindingAssociation); overload;
      Procedure EvaluateOutputsVirtualAbstract;

      Property OutputsTBindingOutput read FBindingOutput;
      Property OutputValueTValue read GetOutputValue write SetOutputValue;
      ...
    End;

where

  • Compile is used to add the array of tBindingAssociations
  • Outputs is used to specify where the result of the evaluation will be stored
  • EvaluateOutputs will start the evaluation


And, for our simple code LiveBinding, we used a simple tBindingExpression descendent :

TBindingExpressionDefault =
  Class(TBindingExpression,
      IScopeIScopeExIScopeEnumeratorIScopeSymbols,
      ICompiledBindingICompiledBindingWrappers)
    Private
      FBindingICompiledBinding;
      FRootScopeIScope;
    Protected
      Property RootScopeIScope read FRootScope implements IScope;
    ...
  End;

and:

  • RootScope will hold the environment (the links between the string identifiers and the token values, the wrappers)
  • fBinding is the parsed expressions


3.5.2 - Creation and compilation of the binding expression

Here is the code:

Var g_c_binding_expressiontBindingExpressionDefaultNil;

  g_c_binding_expression:= c_my_binding_expression_default.Create;

  g_c_binding_expression.Source:= p_source;

  g_c_binding_expression.Compile([
      l_c_person_binding_association,
      l_c_company_binding_association
    ]);



3.5.3 - Displaying the environment scopes

To display the tBindingExpression we neet to know the type of the RootScope implementing object.

First we must make this Protected RootScope visible. We simply define a tBindingExpression descendent with RootScope promoted to Public visibility

Type c_my_binding_expression_default=
       Class(tBindingExpressionDefault)
         Public
           Property RootScope;
       End;

Var l_c_root_scopeiScope;

  l_c_root_scope:=
      c_my_binding_expression_default(g_c_binding_expression).RootScope;

Then we simply convert this RootScope iScope Interface to the implementing object, using any of the Interface to tObject technique :

  display(f_c_interface_to_object(l_c_root_scope).ClassName);



3.5.4 - tNestedScope

In our case, the RootScope is implemented by a tNestedScope object

TNestedScope =
  Class(TInterfacedObjectIInterfaceIScopeIScopeEx,
    IScopeEnumerableIScopeSelf)
    Public
      Property InnerIScope read FInner;
      Property OuterIScope read FOuter;

      Function Lookup(Const NameString): IInterfaceoverload;
    ...
  End;

This tNestedScope is used to build the nested environment. Like in Pascal, in a Procedure you can have a Var which is a Record containing an Array etc.

The Lookup procedure simply is :

Function TNestedScope.Lookup(Const NameString): IInterface;
  Begin
    Result := Inner.Lookup(Name);
    If Result = Nil Then
      Result := Outer.Lookup(Name);
  End;

And this clearly demonstrates what a "Scope" really is.



To display the Inner iScope, we used the same Interface to Object technique, to display the object implementing this Interface. It turns out to be a tDictionaryScope here. So we created a procedure which can display those objects.

The tDictionaryScope contains a tDictionary <String, iInterface>. To analyze the items of a tDictionary, we iterate thru the Keys and use the key value to get the Values. Something like this:

Var l_string_keyString;
    l_i_valueiInterface;
    l_c_valuetObject;

  With p_c_dictionary_scope Do
    With Map Do
      For l_string_key In Keys Do
      Begin
        l_i_value:= Items[l_string_key];
        l_c_value:= f_c_interface_to_object(l_i_value);
        display('  wrapper.ClassName 'l_c_value.ClassName);
      End;



The Interface to Object tells us that the iInterface is implemented by a tObjectWrapper Class:

TObjectWrapper = 
  Class(TInterfacedObject,
      IWrapper,
      IValue,
      ILocation,
      IPlaceholder,
      IWrapperBinding,
      IScopeIScopeExIScopeSelfIScopeEnumerableIScopeSymbols)
    ...
  End;



So we simply have to display this tObjectWrapper. Simple ? Not quite. THIS requires some explainin:



The tObjectWrapper is hidden inside the Implementation part of the System.Bindings.ObjEval Unit. To display this kind of object we can

  • either create a local copy of the definition and use this definition to cast the iInterface
  • or query the iInterface for iValue or iScope (using Supports) to extract the information
We used the second technique, and here is our display procedure

Procedure display_dictionary_scope(p_c_dictionary_scopetDictionaryScope);
  Var l_string_keyString;

      l_i_valueiInterface;
      l_c_valuetObject;

      l_i_the_valueiValue;
      l_i_the_value_valuetValue;
  Begin
    With p_c_dictionary_scope Do
    Begin
      // -- analyze the tDictionary<String, iInterface>
      With Map Do
        For l_string_key In Keys Do
        Begin
          display('Key 'l_string_key);
          l_i_value:= Items[l_string_key];
          l_c_value:= f_c_interface_to_object(l_i_value);
          display('  wrapper.ClassName 'l_c_value.ClassName);

          // -- get the iValue Interface
          If Supports(l_i_valueiValuel_i_the_value)
            Then Begin
                display('  supports ivalue');
                display('    type : 'l_i_the_value.GetType.Name);
                l_i_the_value_value:= l_i_the_value.GetValue;
                display('    value.type 'l_i_the_value_value.TypeInfo.Name);
                If l_i_the_value_value.IsObject
                  Then Begin
                      display('      isObject');
                      display('      the_value_is '
                          + l_i_the_value_value.AsObject.ToString);
                  End;
            End;
        End;
    End// with p_c_dictionary_scope
  End// display_dictionary_scope

Procedure display_nested_scope(p_c_nested_scopetNestedScope);
  Var l_c_dictionary_scopeTDictionaryScope;
  Begin
    With p_c_nested_scope Do
    Begin
      display('p_c_nested_scope.Inner type_name: '
          + f_c_interface_to_object(Inner).ClassName);

      If Inner Is tDictionaryScope
        Then Begin
            l_c_dictionary_scope:= TDictionaryScope(Inner);
            display('Inner');
            display_dictionary_scope(l_c_dictionary_scope);
          End;
    End;
  End// display_nested_scope

  display_nested_scope(l_c_root_scope As tNestedScope);

with the following result (with both g_c_person and g_c_company associations added to the expression) :

display_expression



3.5.5 - The compiling process steps

When we call my_binding_expression.Compile:
  • the association array passed as parameter is stored in the tBindingExpression.Association array:

    Procedure TBindingExpression.Compile(
        Const AssocsArray Of TBindingAssociation);
      Var iInteger;
      Begin
        Clear;
        For i := Low(AssocsTo High(AssocsDo
          Associations.Add(Assocs[i].RealObjectAssocs[i].ScriptObject);

        Compile;
      End;

  • then the Overloaded Compile is created. This procedure builds the RootScope:

    Procedure TBindingExpressionDefault.Compile;
      Var LScopeIScope;
      Begin
        // -- Create nested scopes as needed
        // -- glue together "True", "=" "<=" etc   with True, EqualOp etc
        FRootScope := TNestedScope.Create(BasicOperatorsBasicConstants);
        For LScope In FScopes Do
          LParentScope := TNestedScope.Create(FRootScopeLScope);

        // -- add our associations g_c_person <-> "person"
        If Associations.Count > 0 Then
          LParentScope := TNestedScope.Create(FRootScope,
            CreateScope(Associations));

        // -- from the Source string, build the evaluation tree
        FBinding := System.Bindings.Evaluator.Compile(SourceFRootScope);
      End;

  • the last instruction of Compile builds the expression tree and stores it in fBinding.

    The System.Bindings.Evaluator Unit has this single global Compile method

    Function Compile(Const SourceStringConst RootScopeIScope = Nil): 
        ICompiledBinding;



3.6 - The compiled expression: iCompiledBinding

tBindingExpressionDefault.fBinding stores the compiled expression. However fBinding is a Private member. So we had to use a cast to reach this member:

Type c_bogus_binding_expression=
       Class(TBindingExpression)
         Private
           FBindingICompiledBinding;
         Public
           Function f_i_binding : ICompiledBinding;
       End;

Function c_bogus_binding_expression.f_i_bindingICompiledBinding;
  Begin
    Result:= fBinding;
  End// f_i_binding

Var l_i_compiled_bindingiCompiledBinding;

  l_i_compiled_binding:=
      c_bogus_binding_expression(g_c_binding_expression).f_i_binding;



The fBinding is defined as an iCompiledBinding Interface, which will only be used to call Evaluate for computing the result of an evaluation :

ICompiledBinding = 
  Interface
    Function Evaluate(ARootIScope;
      ASubscriptionCallbackTSubscriptionNotification;
      {out} SubscriptionsTList<ISubscription>): IValue;
  End;

In fact the BindExpression.Compile transforms the string source expression in a pseudo code array (list of elementary operations on the environment), stores this code in the implementing Class, and uses this pseudo code to evaluate different environments when EvaluateOutputs is called.



In our case, the fBinding is implemented by a tCompiledBinding Class :

TCompiledBinding = 
  Class(TInterfacedObjectICompiledBindingICompiledBindingWrappers,
      IDebugBinding)
    ...
  End;

Since this Class is, again, nested in the Implementation of the System.Bindings.Evaluator Unit, we cannot directly analyze it.

We decided to call the tempting iDebugBinding Interface, defined as.

IDebugBinding = 
  Interface
    Procedure Dump(Const WTProc<String>);
  End;

Therefore the dump uses the classic Anonymous Method technique to add debug tracing. Therefore we wrote the following code:

Procedure display_compiled_binding(p_i_compiled_bindingiCompiledBinding);
  Var l_c_objecttObject;
      l_i_debug_bindingiDebugBinding;
  Begin
    l_c_object:= f_c_interface_to_object(p_i_compiled_binding);
    display('comp_bind 'l_c_object.ClassName);

    If Supports(p_i_compiled_bindingIDebugBindingl_i_debug_binding)
      Then Begin
          l_i_debug_binding.Dump(
             Procedure(valueString)
               Begin
                display(value);
               End
            );
        End;
        End;
  End// display_compiled_binding

And here is the result:

display_pseudocode



Please note

  • This result demonstrates what the comment of tCompiledBinding says:

        "An ultra-simple stack machine for evaluating expressions"

  • the call of the


3.7 - Step 3 : Initializing the Outputs

Once the environment has been setup and the pseudo-code computed, we now tell what our output should be:

g_c_binding_expression.Outputs.Add(Label1'Caption');



tBindingOutput is defined by

TBindingOutput =
  Class
    Public
      Type
        TOutputPair = TPair<TObjectString>;
        TDestinations = TDictionary<ILocationTOutputPair>;
    Private
      FOutputsTDestinations;
    Public
      Procedure Add(AObjectTObjectConst PropertyNameString); overload;
      Property DestinationsTDestinations read FOutputs;
    End;

and:

  • Add receives theLabel1 tObject and its property name, Caption
  • the result is stored into Destinations
We can display the output information:

Procedure display_outputs(p_c_outputTBindingOutput);
  Var l_i_locationiLocation;
      l_c_output_pairTBindingOutput.tOutputPair;
      l_valuetValue;
      l_c_objecttObject;
  Begin
    With p_c_output Do
    Begin
      // display(IntToStr(Destinations.Count));
      For l_i_location In Destinations.Keys Do
      Begin
        l_c_output_pair:= Destinations.Items[l_i_location];
        l_value:= l_c_output_pair.Value;
        display('output_pair_property 'l_value.ToString);
        l_c_object:= l_c_output_pair.Key As tObject;
        If l_c_objectNil
          Then display('l_c_object_NIL')
          Else display('output_pair_Object.ClassName 'l_c_object.ClassName'<');
      End;
    End// with p_c_output
  End// display_outputs



3.8 - Step 4 : Evaluating the expression

Finally we call the expression evaluation :

  g_c_binding_expression.EvaluateOutputs;



This method

Procedure TBindingExpressionDefault.EvaluateOutputs;
  Begin
    SetOutputs(
        FunctionIValue
          Begin
            Result := fBinding.Evaluate(FRootScopeNilNil);
          End
      );
  End// EvaluateOutputs

where

  • the Anonymous method calls the fBinding (which is an iCompiledBinding) Evaluate method, which returns an Ivalue
  • this iValue is handed over to SetOutputs which uses its Outputs property to propagate the value to the Label1.Caption


3.9 - The LiveBinding UML Class Diagram

Finally we could draw the following UML Class Diagram :

tbindingexpression_uml_class_diagram

Where

  • in blue everything which is concerned with storing the environment (the identifiers along with their types and input values)
  • in green the main part: tBindingExpression, and the simplified tBindingExpressionDefault
  • the tBindingAssociation is just a temporary helper class used to input the string <-> object relation into the environment
  • tBindingOutput contains the target items of the compiler
  • iCompiledBinding (or tCompiledBinding) contains the pseudo code after the compilation and uses an Evaluate method to transfer the result of the evaluation into the tBindingExpression.Outputs



4 - Comments

4.1 - Interface To Object

To convert any Interface to the tObject which implements this Interface, we used a method f_interface_to_object which is in the U_INTERFACE_TO_OBJECT unit

The technique is around since 2001. I saw it the first time in the Delphi 6 beta news groups. I then wrote the Dump Interface article, back in April 2004.

Several other versions were then published. Hallvard Vassbotn even wrote an article in the Delphi Magazine, in October 2004

Our unit contains the Barry KELLY version, which was an answer to a Stack Overflow question and later changed into a blog post. The reason we used this version is that I did not know (and still have not checked) whether the Delphi 6 (2001) hack was still valid in 2011.



Of course, when we are sure of the type of the implementing object, we can use AS or even directly cast the Interface with the implementing object (which was used in the code above)



4.2 - LiveBinding Compiler in perspective

After all this activity, il all boils down to
  • the BindingExpression builds an environment of all the identifier, storing along with the string the type and value information of each identifier
  • this environment consists of
    • the predefined litterals and operators
    • the identifiers extracted by Compile from the source string expression
  • Compile also builds an "executable" which is a pseudo code array
  • once the Outputs have been specified, the EvaluateOutputs runs the stack machine interpreter to build the result and send it to the outputs. The only difference is that the result can be sent to outputs specified AFTER the compilation. You do not compile an assignment, but you compile an expression, and the resulting value can be sent to any compatible output.


LiveBindings are not a full fledged compiler. Their sole purpose is to link objects together using String Expression.



In this article, we took a VERY low level viewpoint, with a big risk of looking at the tree and not the forest

  • first of all it uses the tBindingExpressionDefault, which is a simplified tBindingExpression for evaluations by code
  • our example did not investigate all the callbacks and notifiers we saw while looking at the sources
  • some functionalities could not be implemented, or we did not understand how to use them. For instance, we could not convert an Integer Property. There surely is are ToStr or Format functions, and our LiveBinding Tutorial did use them (as do some of the SourceForge samples). But we had no success in our tBindingExpressionDefault.
  • analyzing the example tells us what kind of data can be involved in Delphi LiveBindings. In our case tObject Properties to Component properties. Keep in mind however that our example uses the special tBindingExpressionDefault, and we only presented the data and methods involved in THIS example. There are many other properties and Overloaded methods of the involved Classes that we did not present here.


With the previous understanding under our belt, it would be interesting to now try a top down approach, starting for instance from a simple tBindExpression, to try to bridge the gaps. Understanding tBindingScope, how the notification works, what Managers are, exploring the wrappers, adding numeric conversions ...



4.3 - The coding Style

Understanding this code forced us to actually work with Rtti, Generics, Anonymous Methods, and this was certainly long overdue.

From my (old-hand Apple ][ Pascal coder) point of view, the code is a little bit high on the "coding to the Interface" side.

Defining RootScope as an iScope seems somehow an overkill. Like SendertObject for an tWinControl notifications.

This certainly allows to use nearly any type of object which can be looked up as a RootScope. And no doubt, this is all the compiler needs to do: just call Lookup on RootScope. However this makes it more difficult to dump the different objets involved.



Another nostalgic comment would be sophistication of the code. Niklaus WIRTH's Pascal P4 Compiler is about 3500 lines for the compiler and 1500 lines for the interpreter. The LiveBinding sources are around 800 K bytes (granted, bytes, not LOC).

However using the last available technology (Rtti, Generics, Anonymous Methods), LiveBinding allow us to have runtime compilation and late binding.



4.4 - The current documentation

No doubt the whole Embarcadero team was busy as hell during those last months. This perhaps explains the little amount of help about LiveBindings. The presentations are good, and the tutorials well explained. But the details about each class is somehow lacking.

In fact, it only takes 10 minutes to see that the current Wiki (and HLP) documentation is a simple reformatting of the /// and <summary> comments present in the source code. So reading the sources will tell you MUCH more about the details of LiveBindings than looking at the documentation. You have the signatures, the adjacent informations and concepts, the grouping in Units, the Structure Treeview. I can only URGE you to spend an hour or so browsing this code.

For the part we covered here, the Units are

  • System.Rtti
    • tValue
  • System.Generics.Collections
    • tPair
    • tDictionary
    • Proc < String>
  • System.Bindings.EvalProtocol
    • iWrapper
    • iValue
    • iLocation
    • iScope
    • iCompiledBinding
    • tValueWrapper
  • System.Bindings.EvalSys
    • TDictionaryScope
      • MakeBasicConstants ()
      • MakeBasicOperators ()
    • TNestedScope
    • System.Bindings.CustomWrapper
      • tCustomWrapper
  • System.Bindings.Outputs
    • tBindingOutput
  • System.Bindings.Evaluator
    • compile ()
      • tCompiledBinding
      • Evaluate ()
  • System.Bindings.ObjEval
    • WrapObject ()
      • tObjectWrapper
  • System.Bindings.Expression
    • tBindingAssociation
    • tBindingExpression
  • System.Bindings.ExpressionDefaults
    • tBindingExpressionDefault


In fact, to have those nearby I copied them in the project directory (some times the Delphi XE2 was sick and tired of my casting mistakes and "Find In Files" refused to work, and on the other hand the Delphi 6 IDE "Find in Files" stalled on the "System.xxx" names in the Uses clause). So directly loading the units from a file explorer was a shortcut.




5 - Download the Sources

Here are the source code files: The .ZIP file(s) contain:
  • the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
  • any .TXT for parameters, samples, test data
  • all units (.PAS) for units
Those .ZIP
  • are self-contained: you will not need any other product (unless expressly mentioned).
  • for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
  • will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP:
  • create or select any folder of your choice
  • unzip the downloaded file
  • using Delphi, compile and execute
To remove the .ZIP simply delete the folder.

The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.
The .ZIP file(s) contain:

  • the main program (.DPROJ, .DPR, .RES), the main form (.PAS, .ASPX), and any other auxiliary form or files
  • any .TXT for parameters, samples, test data
  • all units (.PAS .ASPX and other) for units
Those .ZIP
  • are self-contained: you will not need any other product (unless expressly mentioned).
  • will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path outside from the container path creation etc).
To use the .ZIP:
  • create or select any folder of your choice.
  • unzip the downloaded file
  • using Delphi, compile and execute
To remove the .ZIP simply delete the folder.

The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.



As usual:

  • please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will be helpful for other readers
  • we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
  • or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
    Name :
    E-mail :
    Comments * :
     

  • and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.



6 - References

  • Interface to Object
  • for Rtti
  • LiveBindings in RAD Studio
      the Delphi WIKI contains presentations about LiveBindings
  • the Delphi XE2 SourceForge repository contains about 10 LiveBinding samples, covering both Vcl and FireMonkey examples, for simple controls and for database LiveBindings

  • In the core of LiveBindings expressions of RAD Studio XE2
      Daniele TETI - Aug 30 2011
        the basic tBindingExpression example, presented just before the Delphi XE2 launch

  • Delphi XE2 LiveBindings Tutorial
      John Colibri - 30 Sept 2011 - 54K 6 sample codes, 43 figs
    • how to setup the SourceComponent and the ControlComponent and expression, tBindingsList, the bindings Editor, using several sources with tBindingScope, building bindings by code, LiveBindings and databases. Far more flexible than the Vcl db_xxx, but with the risks of late binding (in French)
  • FireMonkey Architecture : the basic tComponent <- tFmxObject <- Fmx.tControl <- tStyledControl hierarchy. Firemonkey UML Class diagram, and short feature description.
  • Simple FireMonkey Object Inspector
      Felix COLIBRI - 10 Oct 2011 - 52 K, 2 .ZIP source, 5 Fig
        building a FireMonkey Object Inspector which presents the components of the Form and displays their property names an values and allows the user to modify them at runtime
  • FireMonkey Style Explorer : create tFmxObjects from their class name, create their default style, display their child style herarchy in a tTreeView, present each style element in an Object Inspector which can be used to change the property values.

  • The Pascal P4 Compiler
      Niklaus WIRTH - 1976
        Where it all started ...



7 - The author

Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi Xe_n migrations, refactoring), Delphi Consulting and Delph training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP  /  UML, Design Patterns, Unit Testing training sessions.
Created: oct-11. Last updated: jul-15 - 98 articles, 131 .ZIP sources, 1012 figures
Copyright © Felix J. Colibri   http://www.felix-colibri.com 2004 - 2015. All rigths reserved
Back:    Home  Papers  Training  Delphi developments  Links  Download
the Pascal Institute

Felix J COLIBRI

+ Home
  + articles_with_sources
    + database
    + web_internet_sockets
    + oop_components
      – virtual_constructor
      – generics_tutorial
      – generics_constraints
      – livebindings_spelunking
    + uml_design_patterns
    + debug_and_test
    + graphic
    + controls
    + colibri_utilities
    + colibri_helpers
    + delphi
    + firemonkey
    + compilers
  + delphi_training
  + delphi_developments
  + sweet_home
  – download_zip_sources
  + links
Contacts
Site Map
– search :

RSS feed  
Blog